from google.appengine.ext import db
from app.helpers.simplify import simplify
from app.helpers.strings import rfill
from tags import Tag
import md5, random, time

# How much of a property's representation should we look at?
# Total agg_sort must be < 500, but must have enough for
# sort to be (mostly) unique for each person
# Keep in mind, we also append each entry's key_name to end as a "tie-breaker"
_AGGSORT_LENGTH = 33

class ValueSet(db.Expando):
  """ A set of values for a given fieldset and person 
  
  Note that there is no person object in the datastore. Instead, we
  designate one ValueSet as the primary ValueSet for that person and have
  all other ValueSets for that person link to that one.
  
  Data in the primary ValueSet is denormalized across secondary ValueSets.
  
  Each ValueSet corresponds to a FieldSet. The FieldSet defines how we
  interpret the values in the ValueSet. See app.models.fieldsets for more.
  
  Because of the non-intuitive modelling of ValueSets (at least from a
  relational database point of view, it is recommended that ValueSets
  be interacted with through app.models.valuesets_interface.ValueSetInterface).
    
  """
  # The fieldset determines what fields we use for this valueset
  fieldset_key = db.Property()
  # See FieldSet.model and the create functions below for explanation of these
  _default_fieldset_key = None
  _default_dataset_key = None
  
  # The dataset of the fieldset, denormalized for quicker lookup
  dataset_key = db.Property()
  
  # Is this a primary valueset?
  is_primary = db.BooleanProperty(default=False)
  
  # Key of the primary valueset for this valueset
  # May refer to its own key if primary
  primary_key = db.Property()
  
  # Below are denormalized values -- this allows us to do quick lookups
  # without the need of a JOIN (which AppEngine frowns upon)
  
  # List of strings (mostly) unique to person -- for quick lookup
  # Determined by filterables in primary ValueSet
  lookups = db.StringListProperty()
  # References to fieldsets of all related valuesets
  related_fieldsets = db.ListProperty(db.Key)
  # String unique to person we can sort on
  agg_sort = db.StringProperty()
  # String unique to this ValueSet we can sort on, linked to agg_sort
  unique_agg_sort = db.StringProperty()
  
  def update(self, **values):
    """ Update with dict of values """
    for key, value in values.items():
      setattr(self, str(key), value)
  
  def to_tuples(self):
    """ Returns a list to (verbose_name, name, value) tuples """
    assert hasattr(self, '_field_order'), "No field order provided!"
    return [(field.verbose_name, field.name, getattr(self, field.name))
      for field in self._field_order]
  
  def to_list(self):
    assert hasattr(self, '_field_order'), "No field order provided!"
    values = [getattr(self, field.name) for field in self._field_order]
    return map((lambda v : ', '.join(v) if hasattr(v,'__iter__') else v), values)
  
  def set_primary(self):
    """ Sets a bunch of variables that need to be set if self is primary """
    self.is_primary = True
    
    # This ValueSet is its own primary ValueSet
    self.primary_key = self.key()
    
    # Iterate through fields to get lookups and sort data
    sort_values = []
    lookups = []
    for field in self.__class__._field_order:
      
      if hasattr(field, 'get_value_for_lookup'):
        lookup_value = field.get_value_for_lookup(self)
      else:
        lookup_value = field.get_value_for_datastore(self)
      
      if lookup_value:
        simplified_lookup = simplify(lookup_value)
        if hasattr(simplified_lookup, '__iter__'):
          lookups += simplified_lookup      
        else:
          lookups.append(simplified_lookup)
      
      if hasattr(field, 'get_value_for_sort'):
        sort_value = field.get_value_for_sort(self)
      else:
        sort_value = field.get_value_for_datastore(self)
      
      if sort_value:
        simplified_sort = simplify(sort_value)
        if hasattr(simplified_sort, '__iter__'):
          sort_values += simplified_sort      
        else:
          sort_values.append(simplified_sort or '')
    
    self.lookups = lookups
    self.agg_sort = ''.join([rfill(value, _AGGSORT_LENGTH)
      for value in sort_values])[:500]
    self.set_unique_agg_sort()
  
  def set_secondary(self, primary):
    """ Sets certain values based on changes in primary """
    self.primary_key = primary.key()
    
    if self.fieldset_key not in primary.related_fieldsets:
      primary.related_fieldsets.append(self.fieldset_key)
    
    self.related_fieldsets = primary.related_fieldsets
    self.lookups = primary.lookups
    self.agg_sort = primary.agg_sort
    self.set_unique_agg_sort()
  
  def set_unique_agg_sort(self):
    assert self.agg_sort
    self.unique_agg_sort = self.agg_sort[:(499 - len(self.key().name()))] \
      + self.key().name()
  
  def get_tags(self):
    """ Returns a dict of tag key names to tags that apply to ValueSet """
    tags = {}
    for prop in self.properties().values():
      if hasattr(prop, 'get_value_for_tag'):
        value = prop.get_value_for_tag(self)
        if not hasattr(value, '__iter__'):
          value = [value]
        for v in value:
          if v:
            tag = Tag.make(dataset_key=self.dataset_key,
              fieldset_key=self.fieldset_key,
              prop=prop,
              value=v)
            tags[tag.name] = tag
            
    return tags
  
  @classmethod
  def kind(cls):
    """ All subclasses of ValueSet should use the 'ValueSet' kind """
    return 'ValueSet'
  
  @classmethod
  def from_valueset(cls, valueset):
    """ Given another ValueSet instance, instantiate based on its values
    
    Used to 'typecast' a ValueSet to a subclass
    
    """
    return cls.from_entity(valueset._populate_internal_entity())
  
  @classmethod
  def make_primary(cls, **values):
    """ Make a new primary ValueSet, unsaved -- adds lookups
    
    Args:
      **values : Keyword args for properties to add
    
    Returns:
      A primary ValueSet object
    
    """
    valueset = cls.make_base(**values)
    valueset.set_primary()
    return valueset
  
  @classmethod
  def make_secondary(cls, primary, **values):
    """ Make a new ValueSet, unsaved, from a primary valueset
    
    Args:
      primary : The primary ValueSet with a key
      **values : Keyword args for properties to add
    
    Returns:
      A secondary ValueSet object linked to the primary one
    
    """
    valueset = cls.make_base(primary_key=primary.key(), **values)
    valueset.set_secondary(primary)
    return valueset
  
  @classmethod
  def make_base(cls, primary_key=None, **values):
    """ Custom constructor and whatnot
    
    Args:
      **values : Values to insert into new ValueSet object
    
    Returns:
      A ValueSet object
    
    """
    # Check fieldset and dataset keys
    fieldset_key = values.setdefault('fieldset_key', cls._default_fieldset_key)
    dataset_key = values.setdefault('dataset_key', cls._default_dataset_key)
    
    # Note related fieldsets
    values.setdefault('related_fieldsets', [fieldset_key])
    
    # Create a unique key dependent on dataset and fieldset
    # so we can assign references without trouble
    key_name = cls.get_key_name(fieldset_key, primary_key)
    
    valueset = cls(key_name=key_name, **values)
    
    return valueset
  
  @classmethod
  def get_key_name(cls, fieldset_key, primary_key=None):
    """ Creates a key for a given fieldset and primary
    
    If no primary_key given, assume this is the primary_key and make one up
    """
    if primary_key:
      prefix = 's'
    else:
      prefix = 'p'
      primary_key = str(random.random()) + str(time.time())
    return prefix + md5.new(str(fieldset_key) + str(primary_key)).hexdigest()

